量化交易去掉術語之後,剩下的骨架就是一條後端資料 pipeline。這篇講清楚工程師進場的三個切入點與三個常犯的錯,攤開加密貨幣的資料來源版圖與選型理由,最後把 quantbot 這個範例專案的環境建起來。
人工交易的流程是:盯著圖,覺得看起來要漲了,下單。這裡面每一步都依賴當下的注意力與情緒,而且無法回頭檢驗,因為說不清楚「看起來要漲」的判斷條件到底是什麼。
量化交易做的事情是把同一個流程改寫成一條有明確輸入輸出的 pipeline:
市場資料 → 特徵計算 → 條件判斷 → 訊號(多/空/空手) → 部位大小 → 下單 → 記錄
每一段都是純函式或明確的狀態轉換,所以每一段都能被測試、能被回放、能被換掉。這條 pipeline 就是這 30 天要做的東西。Day 02 到 Day 08 處理最左邊的「市場資料」,Day 09 到 Day 15 處理「特徵計算」,Day 16 到 Day 22 處理「條件判斷」與驗證,Day 23 之後處理右半邊的下單、記錄與不會掛掉。
這條 pipeline 需要的三種能力,剛好都是寫後端服務時常在用的,只是換一個場景。
自動化,對應「條件判斷 → 下單」這一段。 人看盤會累、會漏掉、會在虧了兩筆之後想加碼把錢賺回來。程式不會。凡是能寫成條件的判斷,都可以交給程式在我們睡覺時執行,而且執行得跟清醒時訂下的規則一模一樣。
防禦性設計,對應「下單、記錄」與整個服務的生命週期。 交易程式跑在真錢上,網路會斷、API 會限流、交易所會回 5xx、WebSocket 會靜默斷線。寫任何外部呼叫前先想一輪「它會怎麼壞」,這個習慣在交易系統裡比在一般服務裡更值得堅持,因為這裡壞掉的代價是錢。
數據分析,對應「特徵計算」與策略驗證。 一個策略好不好可以回測、可以量化、可以跟基準並排比較,不必憑感覺。反過來,看到一個好看的數字時,也要先懷疑是不是量測方式有問題。這個懷疑在回測這件事上幾乎是唯一的護欄。
能力講完了,接著講反面。工程師進場出的錯也很固定,而且都是同一個原因:程式跑得出結果,不代表結果是對的。
第一,把回測寫得太樂觀。 最常見的是未來函數,也就是在計算第 t 根 K 線的訊號時,不小心用到了第 t 根之後才會知道的資訊。這種錯不會噴例外,只會讓權益曲線變得非常漂亮。其次是忽略手續費與滑價,理想回測預設我們能用收盤價成交、不用付錢、想買多少有多少,三個假設都是假的。未來函數在 Day 04 第一次正式警告,回測作弊的完整清單與成本模型則在 Day 19 到 Day 20 處理。
第二,把指標算錯卻沒發現。 RSI 的 Wilder 平滑係數是 1/n 而不是 2/(n+1),寫錯了它照樣輸出 0 到 100 的數字,圖畫出來也很像那麼回事。差別要跟現成套件對數字才看得出來。所以這個系列每個自己實作的指標都會配一個對照組,Day 04 到 Day 06 就開始這麼做。
第三,把系統寫得太脆弱。 本機跑得好好的,掛上去第三天凌晨 API 斷線,程式沒有崩潰、也沒有重連,就是安靜地停在那裡不動,而我們以為它還在跑。這比直接掛掉更糟,因為不會收到任何通知。Day 23 整篇在講這件事。
還有一個錯是這套積木庫特有的:組合太多、全部跑一遍、挑回測最好看的那一個。 這在統計上等於自欺,而且積木化之後會變得非常容易做,所以 Day 21 整篇拿來處理它。
這個系列不教怎麼判斷會漲會跌,也不會附上一個能賺錢的策略。這兩件事一個超出範圍,另一個不誠實。
它處理的是另一段:怎麼把一個判斷變成可驗證、可自動執行的系統。 判斷本身從哪來不在範圍內,可能來自讀到的研究、可能來自自己觀察到的規律。這個系列給的是後面那條流程:把判斷寫成程式碼、拿歷史資料檢驗它、加上真實成本再檢驗一次、確認它不是運氣,最後讓它無人值守地跑起來。這套流程換一個判斷還能重跑,這才是它的價值。
不是三個寫死的策略,是一套策略積木庫。
如果每個策略都是一個獨立的 class,三個策略就有三份重複的「算特徵、判斷條件、決定部位」邏輯,而且想試「這個策略的進場條件配那個策略的出場條件」時,完全沒有辦法。所以這個系列從 Day 16 開始把策略拆成可互換的積木:進場條件、過濾條件、出場規則,Day 24 再加上部位計算。新策略的成本從此降到一個設定檔。
先看一眼終點長什麼樣子。這是 Day 17 會產出的設定檔,一個以 EMA 快慢線交叉進場、用 RSI 過濾掉追高的策略:
name: trend_ema_rsi
symbol: BTC/USDT
market: spot # 現貨。標清楚市場類型,不跟永續合約的資料混用
timeframe: 1h
entry:
all_of:
- cross_above:
fast: { feature: ema, period: 12 }
slow: { feature: ema, period: 26 }
filter: # 過濾不負責進場,只負責否決進場
none_of:
- threshold:
feature: { feature: rsi, period: 14 }
op: ">"
value: 70
exit:
any_of:
- cross_below:
fast: { feature: ema, period: 12 }
slow: { feature: ema, period: 26 }
- max_holding_bars: 48
看得懂這份設定檔在說什麼,不需要任何交易背景,這正是積木化想達到的效果。到了 Day 18,換掉 entry 底下那幾行就能得到一個哲學完全相反的均值回歸策略,不必改任何 Python。
不過這裡有一個必須先講的配套。能自由組合,就一定會有人(包括我們自己)拿它亂試一通,跑個五百組挑最漂亮的那一個。所以這套工具必須連安全鎖一起交付,Day 21 會用一份「在完全隨機的假資料上照樣挖得出漂亮策略」的示範把這件事講完。在那之前,這個系列不會鼓勵多試幾組看看。
新手最容易在這裡停很久。搜「crypto data api」,跳出十幾家 provider,每一家的首頁都說自己是機構級、每一家都有價目表,看起來每一家都很重要。實際上它們解決的是完全不同的問題,而這 30 天只需要走其中一條。
先把地圖攤開。
| 類別 | 解決什麼問題 | 代表 | 本系列用不用 |
|---|---|---|---|
| A. 交易所原生 | 我們實際下單那個場所的第一手行情:歷史批次、缺口回補、即時串流 | data.binance.vision、Binance REST、Binance WebSocket | 主力。所有進到專案的市場資料都源自這裡 |
| B. 多交易所聚合 | 跨市場參考價、幣種基本資料、交叉驗證 | CoinGecko、CoinMarketCap、CryptoDataDownload | 只當對照組 |
| C. 機構級 tick 與 L2 歷史 | 跨多家交易所、多年份的逐筆成交與掛單簿重建 | Tardis.dev、Kaiko、CoinAPI、Amberdata | 不用 |
| D. 衍生品專項 | 資金費率、未平倉量、清算資料 | CoinGlass | 不用(Day 30 的演化路徑) |
| E. 鏈上資料 | 錢包標記、協議 TVL、鏈上資金流 | Dune、Nansen、Glassnode、DefiLlama、Etherscan、The Graph | 不用(Day 30 的演化路徑) |
這張表最重要的是最後一欄。五類裡面,這 30 天真正會動到的只有 A,B 只出現在驗證的環節。
第一類裡面交易所有幾十家,選哪一家是有理由的,而且理由要能查證。
一,流動性集中在它身上。 依 CoinGecko 2026 年第二季的交易所報告,前十大中心化交易所的現貨總量約 1.95 兆美元,其中 Binance 佔 38.7%,第二名 Bybit 約 10%。流動性直接決定資料品質:同一段時間裡成交越密集,K 線與掛單簿反映的資訊越完整,第二階段的微觀結構特徵才算得出東西。
二,資料粒度。 Binance 提供 1 秒 K 線,Bybit 與 OKX 的最小間隔是 1 分鐘。對 Day 02 到 Day 08 的日線與小時線來說這沒差別,但 Day 09 之後要處理逐筆成交與掛單簿時,這是實質差距。
三,官方免費的批次資料下載。 data.binance.vision 提供 klines、aggTrades、bookTicker、fundingRate 的 daily 與 monthly zip 檔,免費、無額度限制、不消耗 API weight。這件事的重要性 Day 03 會實際算一次:用 REST 翻頁抓一年份的 1 分鐘 K 線要跑幾百次請求,而下載月檔只要幾個 HTTP GET。
所以在這個系列裡,Binance 資料有三條路徑,各管一段:
| 路徑 | 拿什麼 | 條件 |
|---|---|---|
| data.binance.vision 批次 | 大量歷史 K 線、逐筆成交、掛單簿快照 | 免費、不吃 rate limit。當日資料隔日上傳,月檔次月初才會有 |
| Binance REST(透過 ccxt) | 最近幾天的缺口回補、帳戶與訂單查詢 | klines 單次上限 1000 根、每次 2 weight,每 IP 每分鐘 2400 weight |
| Binance WebSocket(原生 websockets) | 即時逐筆成交、掛單簿增量更新 | 免費,但心跳、重連、序號斷裂後重取快照要自己處理 |
歷史批次跟即時串流是兩條分開設計的路徑,中間的缺口用 REST 補。這個雙軌設計是 Day 03 的主題。
第三類那幾家會在搜尋結果裡排得很前面,價目表也很有存在感。Tardis.dev 提供 150 家以上交易所的 tick 級掛單簿、成交、資金費率、清算與選擇權鏈資料,月費約 50 到 900 美元;Kaiko 與 Amberdata 走合規與機構整合路線;CoinAPI 提供統一 schema 的 REST 歷史查詢加上 WebSocket 歷史重播。
這些服務都很好,但它們解決的是我們現在還沒有的問題:跨多家交易所、重建多年份的 L2 掛單簿,或者做需要精確 bid-ask 的高頻策略。 只要策略頻率在分鐘級以上、而且只在一個交易所下單,Binance 官方免費的批次檔加上 WebSocket 應該就夠用了。
這六條是全系列的選型基準,之後每一天用到資料時都要對得回這裡。
一、先問「這筆資料要拿來做什麼決策」,再選 provider。 決策不同,可接受的延遲、粒度與成本就不同。日線策略需要的東西跟秒級策略差了幾個數量級。
二、能用交易所原生就用原生。 聚合商會重採樣、對齊時間戳、補值,這些加工在回測時是雜訊來源。更重要的理由是:我們實際下單的地方就是那個交易所,訊號用的資料必須跟成交場所一致。
三、現貨資料 NEVER 拿去回測永續合約策略,反之亦然。 現貨是一手交錢一手交幣,永續合約是沒有到期日的槓桿衍生品,兩者的價格、成交量與費用結構都不同(Day 03 會正式解釋)。這是新手回測失真最常見的來源之一,而且症狀很隱蔽:兩邊的價格走勢看起來幾乎一樣,差異藏在不會特別去看的地方。所以文章裡出現交易對時一律標清楚市場類型,設定檔裡也要有 market 這一欄。
四、歷史批次與即時串流是兩條路徑,分開設計。 批次走 data.binance.vision,即時走 WebSocket,中間的缺口用 REST 補。三者的資料在入庫時必須統一 schema 與時區,一律 UTC。
五、每個主來源都要有對照組。 用 CoinGecko 或 CryptoDataDownload 抽樣比對 Binance 的資料,不一致就查清楚原因再往下做。不同 provider 對「時間戳是開盤還是收盤」「成交量是張數還是名目金額」的定義不一樣,接錯了會產生看起來很合理、實際上是假的訊號。唯一的例外是 Day 09 引入的 tick 與掛單簿,那層粒度沒有免費對照組,只能信交易所原生。
六、免費層夠不夠用,動工前先算。 不要寫到 Day 20 才發現要付月費。順帶提一個實際的例子:CoinMarketCap 免費層每分鐘 50 calls、每月 15,000 credits 看起來很夠,但它不含歷史 OHLCV,只有即時報價,所以不能拿它當歷史資料源。CoinGecko 的 Demo 方案每月 10,000 credits、50 個以上的 endpoint,含最多一年的日/時/分 OHLCV,這才是能當對照組的規格。以上額度是 2026 年中的狀態,這類數字半年就會變,動工前自己去官網確認一次。
| 階段 | 主來源 | 對照/備援 |
|---|---|---|
| Day 02–08 基石(K 線、指標、入庫) | data.binance.vision 批次 + REST 補洞 | CoinGecko、CryptoDataDownload 抽樣比對 |
| Day 09–15 微觀結構(Tick、掛單簿) | Binance WebSocket 即時錄製 + 批次的 aggTrades/bookTicker | 無,這層只信原生 |
| Day 16–22 策略與回測 | 前兩階段入庫的 TimescaleDB | 用 CoinGecko 驗證回測期間的價格區間合理 |
| Day 23–29 上線與維運 | Binance testnet(REST + WS) | 正式環境唯讀 API key 做對帳 |
需要的技術先用一張表帶過。每一項的完整理由留給真正用到的那一天,這裡各給一句話就好。
| 用途 | 選型 | 一句話理由 |
|---|---|---|
| 語言 | Python 3.14 | 量化生態系最完整;算得慢的部分交給 numpy 與 Numba |
| 歷史回補 | data.binance.vision | 免費、不吃 rate limit(Day 03) |
| 連線與下單 | ccxt + 原生 websockets | REST signing 不自己手刻,即時資料走原生 WS 才控制得住重連(Day 03、Day 09) |
| 非同步 | asyncio + aiohttp | 爬取與行情訂閱是 I/O bound,不用 threading |
| 資料處理 | pandas + numpy | 全系列一律向量化,NEVER 用 for loop 遍歷 K 線 |
| 儲存 | TimescaleDB + asyncpg | 時序資料要的是 hypertable 分區、時間桶聚合與壓縮(Day 07) |
| 指標 | 自己實作,pandas-ta 當對照組 | 自己算一遍才知道哪裡會錯(Day 04–06) |
| 回測 | VectorBT | 向量化,跟前面的資料處理一脈相承(Day 19) |
| 視覺化 | Plotly + matplotlib | 指標與績效一定要有圖,Plotly 可以縮放看細節 |
| 加速 | Numba | 遞迴型指標向量化不掉時才用(Day 26) |
| 告警 | Telegram Bot API | 免費、有官方 API、手機直接收(Day 25) |
| 設定與密鑰 | pydantic-settings + .env | API key NEVER 寫進程式碼,NEVER 進版控(Day 03) |
| 測試 | pytest | 指標與策略邏輯要有測試,尤其是邊界情況 |
| 部署 | Docker + Docker Compose + VPS | 本機與雲端同一份映像檔(Day 27) |
用 uv(沒裝也可以用 venv 加 pip,指令換掉即可):
uv init quantbot && cd quantbot
uv python pin 3.14
uv add pandas numpy ccxt asyncpg pydantic-settings pyyaml
uv add --dev pytest pytest-asyncio ruff
接著補一段 uv init 不會順手產生的東西。uv init 建出來的是 application 形態的專案,pyproject.toml 裡沒有 [build-system],uv 只會把依賴裝進 .venv,不會把專案本身裝進去。結果是 import quantbot 在某些情境下解析不到——等一下跑測試就會遇到。在 pyproject.toml 末尾加上這三行:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
然後同步一次:
uv sync
輸出裡會看到 + quantbot==0.1.0 (from file:///.../quantbot),代表專案已經以 editable 模式裝進 venv 了。改了程式碼不需要重新安裝,但從此無論從哪個目錄、用哪種方式啟動,import quantbot 都指向同一份原始碼。這件事今天只影響測試跑不跑得起來,到 Day 27 把服務塞進 Docker 時會再受用一次。
目錄不要一次全建好。這 30 天會一天長一塊,今天只需要能跑測試的最小骨架:
quantbot/
├── quantbot/
│ ├── __init__.py
│ └── config.py # 今天唯一的實作
├── tests/
│ └── test_config.py
├── docker/
│ └── docker-compose.yml
├── .env.example
├── .gitignore
├── pyproject.toml # 記得補 [build-system]
└── README.md
從第一天就把設定集中在一個地方,之後每加一個外部服務就往這裡加欄位。API key 一律從 .env 讀,不寫死在程式碼裡:
# quantbot/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""全專案共用的設定。來源是 .env,程式碼裡不出現任何密鑰。"""
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
binance_api_key: str = ""
binance_api_secret: str = ""
binance_testnet: bool = True
postgres_dsn: str = "postgresql://quantbot:changeme@localhost:5432/market"
default_symbol: str = "BTCUSDT"
default_market: str = "spot"
settings = Settings()
binance_testnet 預設 True。這個預設值不是為了今天,是為了三週後改完程式碼、沒注意到自己在動哪個環境的那一次。
.env.example 進版控,.env 不進:
# .env.example
BINANCE_API_KEY=
BINANCE_API_SECRET=
BINANCE_TESTNET=true
POSTGRES_PASSWORD=changeme
POSTGRES_DSN=postgresql://quantbot:changeme@localhost:5432/market
.gitignore 至少要有這幾行:
.env
.venv/
__pycache__/
data/
*.parquet
今天只有一個實作,但它已經值得兩個測試。要測的不是 pydantic 會不會讀 .env,那是套件的事;要測的是這個專案的兩條約定:安全預設沒有被誰翻掉,以及要切環境只能透過環境變數。
# tests/test_config.py
from quantbot.config import Settings
def test_safe_defaults_declared_on_class():
"""預設值宣告在類別上,不受 .env、環境變數與工作目錄影響。"""
assert Settings.model_fields["binance_testnet"].default is True
assert Settings.model_fields["binance_api_key"].default == ""
assert Settings.model_fields["default_market"].default == "spot"
def test_env_can_override(monkeypatch, tmp_path):
"""切正式環境靠環境變數,不靠改 config.py 的預設值。"""
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("BINANCE_TESTNET", "false")
monkeypatch.setenv("BINANCE_API_KEY", "dummy-key")
settings = Settings()
assert settings.binance_testnet is False
assert settings.binance_api_key == "dummy-key"
兩個測試都刻意繞過同一個陷阱:env_file=".env" 是相對路徑,相對於當下的工作目錄。所以第一個測試乾脆不建物件,直接看宣告在類別上的預設值;第二個測試要建物件,就用 monkeypatch.chdir(tmp_path) 先切到一個空目錄,確保那裡沒有 .env 可讀。少了這一步,測試會不會過就取決於本機根目錄那份 .env 寫了什麼、以及 pytest 是從哪個目錄跑起來的——在自己機器上綠燈、CI 上紅燈,多半是這個原因。
第二個測試用的是 Settings(),跟 config.py 最後一行同一個寫法。pydantic-settings 有 Settings(_env_file=None) 這種底線參數可以在單次實例化時關掉讀檔,看起來更省事,但那條路 production 從來不會走,測起來就等於在驗一個實際上用不到的組態模式。
uv run pytest -q
如果這裡噴的是 ModuleNotFoundError: No module named 'quantbot',那就是前面那三行 [build-system] 沒加、或者加了沒 uv sync。這個錯訊很容易讓人以為是目錄結構錯了,其實不是——同一份程式碼,uv run python -m pytest 會過,uv run pytest 會失敗。差別在 -m 會把當下的工作目錄放進 sys.path,而直接跑 pytest 這支 console script 不會;至於 pytest 自己插進 sys.path 的是 tests/(從測試檔往上找第一個沒有 __init__.py 的目錄),不是專案根目錄。所以在專案沒被安裝進 venv 的情況下,兩種跑法的結果會不一樣。
uv sync 之後就不必記這些差異了:專案在 venv 裡,兩種跑法都一樣。
pytest-asyncio 今天用不到,先裝著。Day 03 開始有 async 的擷取邏輯,那時候會用上。
Day 07 才會真的用到資料庫,但今天先把它跑起來,確認環境沒問題:
# docker/docker-compose.yml
services:
timescaledb:
image: timescale/timescaledb:latest-pg16
container_name: quantbot-db
environment:
POSTGRES_USER: quantbot
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme}
POSTGRES_DB: market
TZ: UTC
ports:
- "5432:5432"
volumes:
- timescale-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U quantbot -d market"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
timescale-data:
時區設 UTC 不是細節。交易資料只要有一個環節用了本地時間,之後對帳時會花很久才找到差異在哪。
quantbot 空專案,能跑起來、能跑測試、密鑰不會外洩、README 裡有一張資料源對照表。
驗收標準,四項全過才算完成:
docker compose -f docker/docker-compose.yml up -d 之後,docker compose ps 看到 timescaledb 狀態是 healthy。uv run pytest 跑得動,上面那兩個測試都過(預設值落在 testnet、環境變數蓋得掉預設值)。注意要用 uv run pytest,不是 uv run python -m pytest——前者過了才代表專案真的裝進 venv 了。.env.example 存在且已進版控;.env 存在但 git status 看不到它。第四項看起來像雜務,但它會在 Day 20 之後派上用場。發現某個回測結果不合理時,第一個要問的問題永遠是「這張表是誰給的」,而那時候專案裡已經有三種擷取路徑跟兩個對照組了。
本系列為程式與資料工程的技術分享,所有策略與數字皆為教學範例,不構成投資建議。加密貨幣波動劇烈,實際交易請自行評估風險。系列中所有回測結果都是歷史資料上的表現,不保證未來,也不代表任何策略會賺錢。
明天 Day 02,我們從資料 pipeline 最左邊那一格開始:打開行情圖只看到一堆紅綠棒子,那些棒子其實就是五個數字。我們會把 K 線(OHLCV)拆成熟悉的資料結構,講清楚為什麼是這五個數字、時間戳到底是開盤還是收盤、最後一根 K 線為什麼不能直接拿來用。也會留一個伏筆:這五個數字在壓縮的過程中,丟掉了一整段資訊,那段資訊要到 Day 09 才拿得回來。